package com.oracle.coherence.patterns.security.accesscontrol;

import com.tangosol.util.Base;
import com.tangosol.util.LiteMap;

import java.io.Serializable;
import java.security.Permission;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * The base class for other classes that represent Permsissions.
 * <p/>
 *
 * @author Jonathan Knight
 */
public abstract class BasePermission extends Permission implements Serializable {

    /**
     * The action this permission is for
     */
    private int actionMask;
    /**
     * A flag to indicate whether this permission applies to all caches
     */
    private boolean allTargets;
    /**
     * If this permission applies to specific caches the names are held in this map
     */
    private Map<String, Boolean> caches;

    /**
     * Create a new permission for the specified target cache(s) and for the
     * specified action(s).
     * <p/>
     * The target is either an asterix to represent all caches or a comma delimited
     * list of cache names. Cache names may be suffixed with an asterix to represent
     * wild-carded cache names.<b/>
     * For example<b/>
     * The target <code>test-*</code> matches all cache names that start with <code>test-</code>
     * <p/>
     * The action can be a single action or a comma-delimited list of actions. The action names
     * must match valid names of actions in the {@link com.oracle.coherence.patterns.security.extend.CacheAction} class.
     * <p/>
     *
     * @param sTarget - the target cache this permission is for
     * @param sAction - the action this permission represents
     */
    public BasePermission(String sTarget, String sAction) {
        super(sTarget);
        parseTarget(sTarget);
        actionMask = parseAction(sAction);
    }

    /**
     * Returns the actions as a String. This is abstract
     * so subclasses can defer creating a String representation until
     * one is needed. Subclasses should always return actions in what they
     * consider to be their
     * canonical form. For example, two FilePermission objects created via
     * the following:
     * <p/>
     * <pre>
     *   perm1 = new FilePermission(p1,"read,write");
     *   perm2 = new FilePermission(p2,"write,read");
     * </pre>
     * <p/>
     * both return
     * "read,write" when the <code>getActions</code> method is invoked.
     *
     * @return the actions of this Permission.
     */
    public String getActions() {
        return formatAction(this.actionMask);
    }

    /**
     * Checks if the specified permission's actions are "implied by"
     * this object's actions.
     * <p/>
     * This must be implemented by subclasses of Permission, as they are the
     * only ones that can impose semantics on a Permission object.
     * <p/>
     * <p>The <code>implies</code> method is used by the AccessController to determine
     * whether or not a requested permission is implied by another permission that
     * is known to be valid in the current execution context.
     *
     * @param permission the permission to check against.
     * @return true if the specified permission is implied by this object,
     *         false if not.
     */
    public boolean implies(Permission permission) {
        if (!(permission instanceof BasePermission)) {
            return false;
        }

        BasePermission that = (BasePermission) permission;

        if ((this.actionMask & that.actionMask) != that.actionMask) {
            return false;
        }

        if (this.allTargets) {
            return true;
        }

        if ((that.allTargets) || (that.caches.size() > 1)) {
            throw new IllegalArgumentException("Multiple target permissions cannot be implied");
        }

        if ((that.caches.size() == 1) && (that.caches.containsKey("*"))) {
            return true;
        }

        if (!(this.caches.isEmpty())) {
            for (Map.Entry<String, Boolean> entryThis : this.caches.entrySet()) {
                for (String nameThat : that.caches.keySet()) {
                    if (implies(entryThis, nameThat)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private static boolean implies(Map.Entry<String, Boolean> entryThis, String nameThat) {
        String nameThis = entryThis.getKey();
        if (nameThis.equals("*")) {
            return true;
        }
        Boolean exact = entryThis.getValue();
        return (exact ? nameThat.equals(nameThis) : nameThat.startsWith(nameThis));
    }

    /**
     * Parse the specified comma-delimited list of target caches
     * and set the allTargets or caches fields of this Permission.
     * <p/>
     *
     * @param sTarget - the comma-delimited list of target caches
     */
    @SuppressWarnings({"unchecked"})
    protected void parseTarget(String sTarget) {
        if ((sTarget == null) || (sTarget.length() == 0)) {
            throw new IllegalArgumentException("Target is not specified");
        }

        if (sTarget.equals("*")) {
            this.allTargets = true;
            return;
        }

        this.caches = new LiteMap();

        StringTokenizer tokens = new StringTokenizer(sTarget, ",");

        while (tokens.hasMoreTokens()) {
            String token = tokens.nextToken().trim();

            if (token.endsWith("*")) {
                if (token.length() > 1) {
                    token = token.substring(0, token.length() - 1);
                }
                this.caches.put(token, Boolean.FALSE);
            } else {
                this.caches.put(token, Boolean.TRUE);
            }
        }
    }

    /**
     * Parse the specified comma-delimited list of actions and
     * return the actionMask bit-mask representing
     * the listed actions.
     * <p/>
     *
     * @param actionList - the comman-delimited list of actions.
     * @return the actionMask bit-mask representing
     *         the listed actions
     * @throws IllegalArgumentException - if the actionList parameter is null
     *                                  or an empty string
     */
    protected abstract int parseAction(String actionList);

    /**
     * Returns a String representation of the actions represented by the
     * specified action mask. The action mask is a bit-mapped integer
     * where each bit represents a different action.
     * <p/>
     *
     * @param nAction - the action mask to convert to a string
     * @return a string representation of the specified action mask
     */
    public abstract String formatAction(int nAction);

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BasePermission) {
            BasePermission that = (BasePermission) obj;
            return ((Base.equals(getName(), that.getName())) && (this.actionMask == that.actionMask));
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        int result = getName().hashCode();
        result = 31 * result + actionMask;
        return result;
    }

    protected int parseAction(Action allAction, String actionList) {
        if (actionList == null || actionList.length() == 0) {
            throw new IllegalArgumentException("Action is not specified");
        }

        int mask = 0;
        if (actionList.equals(allAction.getActionName())) {
            mask = allAction.getFlagValue();
        } else {
            StringTokenizer tokens = new StringTokenizer(actionList.toUpperCase(), ",");
            while (tokens.hasMoreTokens()) {
                String sToken = tokens.nextToken().trim();
                Action action = allAction.fromName(sToken);
                mask |= action.getFlagValue();
            }
        }

        return mask;
    }

    protected String formatAction(Action allAction, Action noneAction, int nAction) {
        if (nAction == allAction.getFlagValue()) {
            return allAction.getActionName();
        }

        if (nAction == noneAction.getFlagValue()) {
            return noneAction.getActionName();
        }

        StringBuffer buff = new StringBuffer();
        for (Action action : allAction.actionList()) {
            if (nAction == action.getFlagValue()) {
                buff.append(',').append(action.getActionName());
            }
        }
        return buff.substring(1);
    }

}
